/**
 * Written by Yuval Oren for Attributor. Contributed to the public domain in April, 2010. 
 * Provides Subversion integration with FitNesse using the CM_SYSTEM mechanism available in FitNesse version 20100303
 */

package com.attributor.fitnesse;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNCommitClient;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNStatus;
import org.tmatesoft.svn.core.wc.SVNStatusClient;
import org.tmatesoft.svn.core.wc.SVNStatusType;
import org.tmatesoft.svn.core.wc.SVNWCClient;
import org.tmatesoft.svn.core.wc.SVNWCUtil;

public class SubversionCmSystem {
	private static List<String> ignoredPaths = new ArrayList<String>();
	private static Map<String,SVNClientManager> clientManagerCache = new HashMap<String,SVNClientManager>();
	private static Pattern payloadPattern = Pattern.compile(".+?\\s+(.*?)/(.*?)\\s*"); // class svnuser/svnpassword
	private static String hostname;
	private static String workingDir;

	static {
		ignoredPaths.add("/RecentChanges/");
		ignoredPaths.add("/ErrorLogs/");
		
		try {
			hostname = InetAddress.getLocalHost().getHostName();
		}
		catch (UnknownHostException e) {
			hostname = "(unknown host)";
		}
		
		try {
			workingDir = new File(".").getCanonicalPath();
		} catch (IOException e) {
			workingDir = "(unknown directory)";
		}
		
		
		// SVN setup
		DAVRepositoryFactory.setup();
		SVNRepositoryFactoryImpl.setup();
		FSRepositoryFactory.setup();
	}

	/**
	 * Called before saving the changed file
	 * @param file path to file
	 * @param payload contents of CM_SYSTEM
	 * @throws IOException
	 * @throws SVNException
	 */
	public static void cmEdit(String file, String payload) throws IOException, SVNException {
	    if (isIgnored(file)) {
	      return;
	    }

	    File f = new File(file);
	    SVNClientManager svn = getClientManager(payload);
	    
	    if (svnUpdate(svn,f)) {
	    	throw new IllegalStateException("The page was out of date, as there was a newer revision in subversion. Please try again.");
	    }
	    	
	    
	    SVNStatus status = svn.getStatusClient().doStatus(f, true);
	    SVNStatusType contentStatus = status.getContentsStatus();
	    if (contentStatus == SVNStatusType.STATUS_ADDED ||
	    	contentStatus == SVNStatusType.STATUS_MODIFIED ||
	    	contentStatus == SVNStatusType.STATUS_IGNORED ||
	    	contentStatus == SVNStatusType.STATUS_NORMAL ||
	    	contentStatus == SVNStatusType.STATUS_NONE) {
	    	
	    	// ready to check in
	    }
	    else if (contentStatus == SVNStatusType.STATUS_UNVERSIONED) {
	    	svnAdd(svn,f);
	    }
	    else {
    		throw new UnsupportedOperationException("Don't yet know how to deal with subversion status " + status.getContentsStatus());
	    }
	}


	/**
	 * Called after the file is saved
	 * @param file path to file
	 * @param payload contents of CM_SYSTEM
	 * @throws IOException
	 * @throws SVNException
	 */
	public static void cmUpdate(String file, String payload) throws IOException, SVNException {
		if (isIgnored(file)) {
			return;
		}
		
		svnCommit(file, payload);
	}


	/**
	 * Called before the file is deleted
	 * @param file path to file
	 * @param payload contents of CM_SYSTEM
	 * @throws IOException
	 * @throws SVNException
	 */
	public static void cmPreDelete(String file, String payload) throws IOException, SVNException {
		if (isIgnored(file)) {
			return;
		}
			
		SVNWCClient wcc = getClientManager(payload).getWCClient();
		wcc.doDelete(new File(file), true, false);
	}

	/**
	 * Called after the file is deleted
	 * @param file path to file
	 * @param payload contents of CM_SYSTEM
	 * @throws IOException
	 * @throws SVNException
	 */
	public static void cmDelete(String file, String payload) throws IOException, SVNException {
		if (isIgnored(file)) {
			return;
		}
		
		svnCommit(file,payload);
	}

	
	
	
	/**
	 * Refreshes a file to the latest committed version
	 * @param file path to file
	 * @param payload contents of CM_SYSTEM
	 * @return true if the file changed, false if it was already current
	 * @throws SVNException
	 * @throws IOException
	 */
	public static boolean svnUpdate(String file, String payload) throws SVNException, IOException {
		return svnUpdate(getClientManager(payload), new File(file));
	}


	// *** Other subversion commands ***
	
	private static void svnAdd(SVNClientManager svn, File f) throws SVNException {
		SVNWCClient wcc = svn.getWCClient();
		wcc.doAdd(f, true, true, true, SVNDepth.INFINITY, false, true);		
	}


	private static boolean svnUpdate(SVNClientManager svn, File f) throws SVNException {
		SVNStatusClient sc = svn.getStatusClient();
		long oldRev = sc.doStatus(f, false).getCommittedRevision().getNumber();
		svn.getUpdateClient().doUpdate(f,SVNRevision.HEAD,SVNDepth.INFINITY,true,true);
		long newRev = sc.doStatus(f, false).getCommittedRevision().getNumber();

		return oldRev != newRev;
	}

	
	private static void svnCommit(String file, String payload)
			throws IOException, SVNException {
		// Commit
		SVNCommitClient cc = getClientManager(payload).getCommitClient();
		cc.doCommit(new File[]{new File(file)}, false, "FitNesse checkin from " + hostname + ":" + workingDir, 
				null, null, false, false, SVNDepth.INFINITY);
	}


	private static boolean isIgnored(String filePath) {
		File currentFile = new File(filePath);
		String absolutePath = currentFile.getAbsolutePath();

		for (String ignoredItem : ignoredPaths) {
			if (absolutePath.contains(ignoredItem))
				return true;
		}

		return false;
	}

	private static synchronized SVNClientManager getClientManager(String payload) throws IOException {
		SVNClientManager mgr = clientManagerCache.get(payload);
		if (mgr != null) {
			return mgr;
		}
		
		Matcher payloadMatcher = payloadPattern.matcher(payload);
		
		if (!payloadMatcher.matches()) {
			throw usage();
		}
		
		String user = payloadMatcher.group(1);
		String password = payloadMatcher.group(2);
		
		mgr = SVNClientManager.newInstance(SVNWCUtil.createDefaultOptions(false),user,password);
		clientManagerCache.put(payload, mgr);
		return mgr;
	}
	
	private static IllegalArgumentException usage() {
		return new IllegalArgumentException("CM_SYSTEM should be: " + SubversionCmSystem.class.getName() + " user/password");
	}

}
